import { Event, EventEmitter } from "vscode";
import { traceVerbose } from "../../../../logging";
import { areSameEnv } from "../../info/env";
import { BasicEnvInfo, EQEnvUpdatedEvent, EQLocatorQuery, IEQEnvsIterator, ILocator } from "../../locator";
import { EQEnvsChangedEvent } from "../../watcher";
import { cloneDeep, isEqual, uniq } from 'lodash';
import { EQEnvKind } from "../../info";

export class EQEnvsReducer implements ILocator<BasicEnvInfo> {
    public get onChanged(): Event<EQEnvsChangedEvent> {
        return this.parentLocator.onChanged;
    }

    constructor(private readonly parentLocator: ILocator<BasicEnvInfo>) { }

    public iterEnvs(query?: EQLocatorQuery): IEQEnvsIterator<BasicEnvInfo> {
        const didUpdate = new EventEmitter<EQEnvUpdatedEvent<BasicEnvInfo> | null>();
        const incomingIterator = this.parentLocator.iterEnvs(query);
        const iterator = iterEnvsIterator(incomingIterator, didUpdate);
        iterator.onUpdated = didUpdate.event;
        return iterator;
    }
}

async function* iterEnvsIterator(
    iterator: IEQEnvsIterator<BasicEnvInfo>,
    didUpdate: EventEmitter<EQEnvUpdatedEvent<BasicEnvInfo> | null>,
): IEQEnvsIterator<BasicEnvInfo> {
    const state = {
        done: false,
        pending: 0,
    };
    const seen: BasicEnvInfo[] = [];

    if (iterator.onUpdated !== undefined) {
        const listener = iterator.onUpdated((event) => {
            state.pending += 1;
            if (event === null) {
                state.done = true;
                listener.dispose();
            } else if (event.update === undefined) {
                throw new Error(
                    'Unsupported behavior: `undefined` environment updates are not supported from downstream locators in reducer',
                );
            } else if (seen[event.index] !== undefined) {
                const oldEnv = seen[event.index];
                seen[event.index] = event.update;
                didUpdate.fire({ index: event.index, old: oldEnv, update: event.update });
            } else {
                // This implies a problem in a downstream locator
                traceVerbose(`Expected already iterated env, got ${event.old} (#${event.index})`);
            }
            state.pending -= 1;
            checkIfFinishedAndNotify(state, didUpdate);
        });
    }

    let result = await iterator.next();
    while (!result.done) {
        const currEnv = result.value;
        const oldIndex = seen.findIndex((s) => areSameEnv(s, currEnv));
        if (oldIndex !== -1) {
            resolveDifferencesInBackground(oldIndex, currEnv, state, didUpdate, seen).ignoreErrors();
        } else {
            // We haven't yielded a matching env so yield this one as-is.
            yield currEnv;
            seen.push(currEnv);
        }
        result = await iterator.next();
    }
    if (iterator.onUpdated === undefined) {
        state.done = true;
        checkIfFinishedAndNotify(state, didUpdate);
    }
}

async function resolveDifferencesInBackground(
    oldIndex: number,
    newEnv: BasicEnvInfo,
    state: { done: boolean; pending: number },
    didUpdate: EventEmitter<EQEnvUpdatedEvent<BasicEnvInfo> | null>,
    seen: BasicEnvInfo[],
) {
    state.pending += 1;
    // It's essential we increment the pending call count before any asynchronus calls in this method.
    // We want this to be run even when `resolveInBackground` is called in background.
    const oldEnv = seen[oldIndex];
    const merged = resolveEnvCollision(oldEnv, newEnv);
    if (!isEqual(oldEnv, merged)) {
        seen[oldIndex] = merged;
        didUpdate.fire({ index: oldIndex, old: oldEnv, update: merged });
    }
    state.pending -= 1;
    checkIfFinishedAndNotify(state, didUpdate);
}

function checkIfFinishedAndNotify(
    state: { done: boolean; pending: number },
    didUpdate: EventEmitter<EQEnvUpdatedEvent<BasicEnvInfo> | null>,
) {
    if (state.done && state.pending === 0) {
        didUpdate.fire(null);
        didUpdate.dispose();
    }
}

function resolveEnvCollision(oldEnv: BasicEnvInfo, newEnv: BasicEnvInfo): BasicEnvInfo {
    const [env] = sortEnvInfoByPriority(oldEnv, newEnv);
    const merged = cloneDeep(env);
    merged.source = uniq((oldEnv.source ?? []).concat(newEnv.source ?? []));
    return merged;
}

function sortEnvInfoByPriority(...envs: BasicEnvInfo[]): BasicEnvInfo[] {
    // TODO: When we consolidate the EQEnvKind and EnvironmentType we should have
    // one location where we define priority.
    const envKindByPriority: EQEnvKind[] = getPrioritizedEnvironmentKind();
    return envs.sort(
        (a: BasicEnvInfo, b: BasicEnvInfo) => envKindByPriority.indexOf(a.kind) - envKindByPriority.indexOf(b.kind),
    );
}

function getPrioritizedEnvironmentKind(): EQEnvKind[] {
    return [
        EQEnvKind.Conda,
        EQEnvKind.Unknown,
    ];
}